系列:「一人艦隊:30 天用一群 AI CLI 打造跨平台代理 mesh」— 第 15 天
紀錄日期:2026-09-08;隨本日實作更新。本文目前為草稿。

昨天讓新 AI 讀一份文件就能報到。今天接著問:當 Mac、Windows、Linux、Android 和 Web 由不同機器、不同 AI 協作開發,怎麼避免每個版本各有一套規則?
我們的目標仍是讓使用者帳號下的設備透過 Spectyn Mesh 協作,接入 AI CLI 和子專案能力。分工可以不同,但「工作是否已保存」「按停止之後到底停了沒有」必須有共同定義。只共用 React 元件,不足以保證這些行為。
因此先把三件事拆開:執行環境決定如何接到系統;布局決定畫面如何排列;能力與授權決定現在允許做哪些事。例如 Windows 觸控筆電有觸控螢幕,也應保留桌面版功能;瀏覽器不能直接執行本機 CLI,需要透過已授權的執行端。

讀程式時發現 useIsMobile 把小於 768 像素的視窗判成手機。這個結果在 App.tsx 不只影響排版,還決定使用哪棵路由樹,以及是否顯示桌面啟動檢查。
也就是說,在桌面瀏覽器或允許小於 768 像素的視窗裡拖窄畫面,可能卸載原本的桌面對話元件,改開另一個手機頁面。尚未送出的文字跟著元件生命週期消失;若啟動檢查仍在進行,改走手機分支還會略過它。
回歸測試可在同一台電腦重現這個前端邏輯。不過重新核對打包設定時,發現目前原生主視窗設定的最小寬度是 800,通常無法直接拖到觸發斷點;因此不能把前端重現寫成已在已安裝 Mac App 親手重現。第一包便先修正這個共同語意的缺口。

我先加入回歸案例,再跑原本實作。12 項測試中有 7 項失敗,包含窄窗辨識、啟動檢查與對話畫面保留。之後移除用寬度切換手機/桌面程式的判斷,讓同一次掛載維持相同的畫面分支,窄版排列由既有 responsive CSS 處理。
| 案例 | 驗證內容 |
|---|---|
| 窄窗第一次啟動 | 桌面啟動檢查仍會出現 |
| 檢查期間拖窄 | 不切到手機路由略過檢查 |
| 對話中縮小再放大 | 同一個輸入元件與未送出文字保留 |
| Windows 觸控、Mac、Linux | 跨越布局斷點仍辨識為桌面 |
| 既有手機路由 | 派送、歷史、設定等路由仍通過測試 |
合併既有手機路由回歸後,3 個測試檔共 23 項通過。npm run build 的 TypeScript 檢查與 Vite 建置也成功。建置仍有大型 bundle 的提示,這包沒有處理體積最佳化。
這些是前端路由與元件測試:對話頁、啟動檢查的內部服務有替身。它能證明縮窗不再切換該路由與丟失這份畫面狀態,還不能證明真 AI 工作已保存、所有原生平台可用,或整個 App 已完成。

第二包往任務流程繼續追。讀取對話歷史已經有避免舊回應覆蓋新畫面的處理,但任務清單與送出失敗後的任務查詢還沒有。網路或本機服務較慢時,切到第二個對話之後,第一個對話的任務可能才回來,讓畫面顯示錯誤的工作,甚至讓停止按鈕指向舊任務。
先加四個回歸測試:跨對話的慢回應、同對話兩次讀取倒序完成、送出新工作前發出的舊查詢,以及失敗後查詢尚未完成就切換對話。原本實作四項全部失敗。
修正沿用現有元件,替任務查詢加上遞增版本。切換對話、開始較新的查詢或送出新工作,會讓較舊的回應失效;回應抵達時,只有仍符合目前版本才能改寫畫面和停止目標。沒有重新送出 prompt,也沒有修改後端任務資料。
非作者審查又指出第五個情境:本機工作送出後收到 Runtime 就緒事件,重新整理可能覆蓋正在執行的工作。我先補測試確認失敗,再讓本機 durable 工作執行期間暫不重新載入歷史與任務清單,保留送出工作自己的完成與恢復流程。
第二包的對話測試共 9 項通過;加上第一包與手機路由回歸,第二包完成時為 4 個檔案共 32 項通過。停止測試檢查傳給 bridge 的 task ID 是本次送出的 ID,並確認沒有送往舊 ID。這能驗證前端不誤選該停止目標,真正 runner 是否收到並確認停止仍要另外驗證。

前兩包修正的前端測試完成後,我重新打包 Mac debug App,包含當前 Rust 執行核心。打包成功後正常退出舊 App,確認它持有的執行程序也結束,再開啟新包。啟動自檢通過,既有紀錄仍能讀取。
這次新增兩個專用測試對話 A、B,各自送一個不使用工具的簡短 AI 請求。兩邊都收到真實回覆,唯讀檢查資料庫的任務也都是 completed、歷史 saved、runner_acknowledged = 1。接著退出再開,從清單分別選 A、B,兩邊的問答各自完整,沒有重新送出請求。
接著在 B 送一個不使用工具的計數請求,按停止。畫面先顯示等待確認,資料庫當時仍是 running,只是 stop_requested = 1;等待後進入 cancelled / saved,執行程序確認欄位變成 1。但這還不能證明生成被提早中止。
收尾時核對完整資料庫輸出,發現第一次要求 500 行,實際保存了完整 500 行;最新測試要求 800 行,也保存了完整 800 行,整筆任務分別約花 45 秒與 91 秒。先前畫面擷取只顯示到第 155 行,是擷取內容被截斷,不能當成部分輸出的證據。程式追查也吻合:本機任務等待完整模型回覆時沒有及時觀察取消訊號,回覆回來後才依停止旗標標成取消。停止要求有保存,但提早中止模型等待的驗收未通過。
| 原生操作 | 本次觀察 |
|---|---|
| 退出與重開 | App 與它持有的執行程序退出;新啟動自檢通過 |
| 對話 A、B | 各自真 AI 回覆,保存後可重新讀取 |
| 要求停止 | 要求有保存,但完整回覆返回後才標成取消;提早中止未通過 |
| 先前 A/B 測試重開 | 原三個任務 ID 與終態不變,沒有新增重跑任務;不含最新 800 行測試 |
這次可證明的是這份 Mac 測試包的簡短對話、保存與停止要求紀錄;真正提早中止仍未通過。它還不是完整工作場景、簽章安裝包或五平台驗收;這次原生測試當下,重開仍回到預設對話,需要手動選回 A、B。
因此第五包補上記住最後選中的對話:重新掛載時讀回選擇,再查詢歷史與任務,不重新送出訊息;任務讀取尚未完成仍不能送出新工作。儲存失敗會提示使用者,但仍可切換對話;登出則清除這項裝置上的偏好。只存對話識別,不把訊息內容或任務狀態寫入瀏覽器儲存。
這包新增六個回歸案例,修正前四個失敗,修正後相關測試共 4 個檔案、55 項通過。重新掛載測試不能取代真正退出與重開 App:22:15 已使用包含前七包修正的 Mac App 實測:選中 B、正常退出、確認 App 與 sidecar 都退出,再啟動並進入對話頁,直接恢復 B 與先前保存的內容。唯讀資料庫仍是相同三筆任務 ID 與終態,沒有新增重跑。這補上最後選中對話的原生驗證;斷線與格式錯誤仍只有元件測試。
22:20 又用第八包測試執行中退出:在獨立對話發出只數數、不使用工具的請求,唯讀資料庫確認是 running / pending,隨即正常退出。確認 App 與 sidecar 都已退出後重開,同一筆任務成為 unknown / unknown,畫面顯示未確認結果,沒有自動重跑,也沒有新增任務 ID。這是本機 runner 隨 App 退出的實測,不代表所有外部 AI CLI 都能被停止。
這次原生測試也直接找出第九包:已有未知任務時,畫面仍顯示首次使用的目標範例。現在只有歷史與任務都確認就緒、沒有恢復收據,而且確實是空對話,才顯示歡迎引導;兩個新回歸先失敗再通過,累計 79 項。新包再次打開同一筆未知任務,已不再顯示首次使用引導;按重新整理後仍是原任務,也沒有重送。這是讓畫面正確表達現況,並沒有把未知結果改成成功。

第三包處理啟動與重新整理時的空窗。原本讀取任務失敗會直接被忽略,回應沒有 tasks 陣列也被當作空清單。因此,畫面還不知道是否有任務在執行,使用者就可能送出下一個工作。
現在先明確顯示「正在確認此對話的任務」。讀取失敗或格式不符合契約時,保留歷史可讀,暫停發送與清除,並提供「重試讀取任務」。重試只查詢現況,不會重新送出 prompt。若同時存在未知結果與執行中的工作,先呈現執行中的任務,停止按鈕才會指向那一筆。
這包也再次證明非作者審查的必要:第一版在任務清單回傳空陣列時解除忙碌,會連帶影響仍在等待回覆的 Provider/夥伴模式。審查指出後,我補了兩個延遲回覆及延遲保存的測試,確認問題存在,再讓三種前景模式都持有忙碌狀態直到回覆與保存處理結束。取得串流 handle 不等於完成,不能提前解鎖。
| 狀態 | 目前行為 |
|---|---|
| 任務清單尚未讀完 | 顯示讀取中,暫停發送與清除 |
| 讀取失敗/格式錯誤 | 顯示原因,允許重新讀取 |
| 恢復出執行中的任務 | 保持忙碌,停止作用於該任務 ID |
| 有效空清單 | 歷史也讀取成功後,允許送出新的工作 |
| 前景回覆或保存尚未結束 | 不被 Runtime 就緒通知提前解鎖 |
第三包完成時,相關回歸共 4 個測試檔、42 項通過,其中對話元件 19 項。這些新增錯誤與延遲情境使用 bridge 替身。第三包當時已打包成功;後續新版也通過上節的正常重開實測。但這批格式錯誤、延遲與儲存失敗分支,仍不能直接套用正常路徑的原生驗收結果。
第七包把歷史也納入門檻。即使任務清單已讀完,如果歷史仍在讀取、讀取失敗或格式不符,三種模式都先暫停發送與清除,避免把遺漏上下文的對話交給 AI。重新整理失敗時保留已顯示的內容,提供「重試讀取對話」,不送出新 prompt。
新增九個案例,原碼有八個失敗;修正後,再補三種模式忙碌時不能切換對話的回歸,累計 4 個檔案、74 項通過。合法的空歷史可以正常使用;但這種格式檢查無法識別 Web 相容層回傳的合法假空值,真實端點接線仍需完成。

第四包接著處理已經恢復出的執行中任務。原本定期查詢失敗時,畫面會解除忙碌,但它其實沒有收到任何停止或完成證據。更麻煩的是,重新整理後若任務 ID 與狀態都一樣,輪詢不會重新啟動,畫面可能就停在舊狀態。
現在查詢失敗會留下已知任務及停止目標,顯示錯誤與「重試查詢任務」。重試使用同一個任務 ID,不重新送出 prompt。重新讀取清單,即使仍是相同任務、相同狀態,也會重新開始查詢。
回應還要核對目前的對話與任務識別。收到另一筆任務、另一個對話或無法辨識的狀態,不能直接套到目前畫面。收到有效完成/取消回應後會解除忙碌;歷史已保存時,也會重新載入成果。明確的未知結果仍以未知狀態呈現,不自動重跑;查詢失敗本身不會被當作任務終態。
| 回歸情境 | 修正後的結果 |
|---|---|
| 執行中查詢斷線 | 保留已知任務、停止按鈕及忙碌狀態 |
| 同任務重新整理 | 可以重新啟動查詢 |
| 回傳其他任務或對話 | 顯示查詢錯誤,不替換目前任務 |
| 取消且保存完成 | 重新讀回已保存回覆 |
這七個新增案例先在原碼失敗,修正後通過;相關回歸累計為 4 個測試檔、49 項。它們是可控制斷線與錯誤回應的元件測試,還需要原生環境的故障驗證,不能當成所有異常恢復都已完成。
第六包補上另一個入口:送出後沒收到回應,也要用相同方式核对補查結果。現在必須確認對話與任務 ID,以及必要狀態欄位;空回應、其他任務或無法辨識的狀態都不會套到畫面。確認前暫停新訊息與清除;補查失敗可重試,而且查的是原本送出的 UUID,不會改用空清單丟掉唯一收據。切換對話後,舊查詢才抵達的錯誤也不會污染新畫面。
七個新增案例中,修正前六個失敗;修正後累計 4 個測試檔、62 項通過。這是受控的元件補查測試;未確認 UUID 的本機補查線索目前只保留在此對話元件,切換對話或卸載後會消失;失去連線後退出 App、再次啟動的恢復仍有缺口。
第八包再補成果顯示:任務已完成,但對話歷史保存失敗或尚未完成時,仍要顯示任務收據中已保存的文字回覆。非文字的異常結果則不直接放進 React,避免整個畫面崩潰。三個新增案例先失敗再通過,累計回歸 77 項;這項修正讓成果可讀,還沒有新增歷史修復或後端重試能力。

跨平台規劃目前整理了 32 個生命週期案例,涵蓋安裝、啟動、對話、任務、停止、重開、配對與更新。32 是檢查清單的案例數,並不是已通過的測試數。
下一個優先修正是模型等待期間的取消,以及停止後不得開始新的工具操作。HTTP 等待與外部 CLI 的清理方式不同,不能只讓畫面顯示取消就算完成;需要用延遲回應與工具副作用測試確認。之後再從簡短對話走到實際工作,驗證故障恢復與未知結果的處置,並把共同生命週期接到其他平台。手機版目前仍有獨立的對話及停止流程,Web 的部分相容介面也仍回傳空值;這些都是要收斂的實碼缺口。
桌面、手機與 Web 可以有不同布局及系統能力,但不能用空資料冒充成功,也不能把「按鈕變回可按」當作工作真的停止。每完成一包,就留下來源、測試結果與限制,再交給下一台機器接續。